﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using Microsoft.Phone.Controls.Maps;

namespace PushpinClusterer
{
    public class PushpinClusterer<T> : FrameworkElement where T : IClusteredGeoObject
    {
        private Map _map;
        private List<T> _pins;
        private double _distanceThreshold;

        public event EventHandler<EventArgs> ClusteringCompleted;
        public ObservableCollection<ClusteredPushpinItem<T>> PushpinModels { get; set; }


        /// <summary>
        /// Clusters the given pins on the supplied map
        /// </summary>
        public PushpinClusterer(Map map, List<T> pins, double distanceTreshold)
        {
            _map = map;
            _pins = pins;
            _distanceThreshold = distanceTreshold;
            PushpinModels = new ObservableCollection<ClusteredPushpinItem<T>>();

            _map.ViewChangeEnd += (s, e) => RenderPins();
        }


        /// <summary>
        /// Re-render the pushpins based on the current zoom level
        /// </summary>
        public void RenderPins()
        {
            PushpinModels.Clear();

            List<PushpinContainer<T>> pinsToAdd = new List<PushpinContainer<T>>();

            // consider each pin in turn
            foreach (var pin in _pins)
            {
                var newPinContainer = new PushpinContainer<T>(pin,
                  _map.LocationToViewportPoint(pin.Coordinate));

                bool addNewPin = true;

                // determine how close they are to existing pins
                foreach (var pinContainer in pinsToAdd)
                {
                    addNewPin = true;
                    double distance = ComputeDistance(pinContainer.ScreenLocation, newPinContainer.ScreenLocation);

                    // if the distance threshold is exceeded, do not add this pin, instead
                    // add it to a cluster
                    if (distance < _distanceThreshold)
                    {
                        pinContainer.Merge(newPinContainer);
                        addNewPin = false;
                        break;
                    }
                }

                if (addNewPin)
                {
                    pinsToAdd.Add(newPinContainer);
                }
            }

            foreach (var projectedPin in pinsToAdd.Where(pin => PointIsVisibleInMap(pin.ScreenLocation, _map)))
            {
                PushpinModels.Add(projectedPin.SelectContainerItem());
            }

            OnClusteringCompleted(null);
        }

        /// <summary>
        /// Gets whether the given point is within the map bounds
        /// </summary>
        private static bool PointIsVisibleInMap(Point point, Map map)
        {
            return point.X > 0 && point.X < map.ActualWidth &&
                    point.Y > 0 && point.Y < map.ActualHeight;
        }

        /// <summary>
        /// Computes the cartesian distance between points
        /// </summary>
        private double ComputeDistance(Point p1, Point p2)
        {
            return Math.Sqrt((p1.X - p2.X) * (p1.X - p2.X) + (p1.Y - p2.Y) * (p1.Y - p2.Y));
        }

        protected virtual void OnClusteringCompleted(EventArgs e)
        {
            EventHandler<EventArgs> handler = ClusteringCompleted;
            if (handler != null)
            {
                handler(this, e);
            }
        }
    }
}
